//
// PropsManager
//
// Copyright (C) jextra.net.
//
//  This file is part of the JavaProps Library.
//
//  The JavaProps Library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The JavaProps Library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the JavaProps Library; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.props;

import java.io.*;
import java.sql.*;
import java.util.*;

/**
 * The PropsManager is the basic functionality for the Props class. It is a singleton that is
 * usually accessed via the {@link Props} class with static methods than directly.
 */
public class PropsManager
{
    // ============================================================
    // Fields
    // ============================================================

    /**
     * "Runtime" properties are properties that are added via the PropsManager method
     * {@link #setString(String, String)}. These are always first and override any subsequent
     * property sources.
     */
    public static final String PROPSOURCE_RUNTIME = "<RUNTIME>";

    /**
     * "System" properties are collected via the <code>System.getProperties()</code>. These are
     * typically properties set as -D values on the javaVM command-line.
     */
    public static final String PROPSOURCE_SYSTEM = "<SYSTEM>";

    /**
     * The "Database" properties is a special property source that is most often bootstrapped via 
     * setting the following properties either in a RUNTIME,SYSTEM, or file-based property source.
     * The four properties define a connection to a JDBC database that contains properties to be
     * read from.
     * <pre>
     * props.db.driver
     * props.db.url
     * props.db.user
     * props.db.password
     * </pre>
     */
    public static final String PROPSOURCE_DATABASE = "<DATABASE>";

    /**
     * Default name of the database table to read from the DATABASE property source.
     * This may be overridden via the <code>props.db.table</code> property.
     */
    public static final String PROPVALUE_DATABASE_TABLE = "props.Property";

    // Singleton instance of the factory. Get to this via static getInstance method.
    private static PropsManager _instance;

    private PropsCipher _cipher;

    private LinkedHashMap<String,Properties> _propertySources;
    private Properties _runtimeProperties;
    private String[] _standardFilenames = 
    {
        "java.properties", "environment.properties"
    };
    private String _dbTable;

    // ============================================================
    // Constructors
    // ============================================================

    protected PropsManager()
    {
        _propertySources = new LinkedHashMap<String,Properties>();

        //
        // Add "runtime" properties as the first Properties file.
        //
        _runtimeProperties = new Properties();
        addProperties( PROPSOURCE_RUNTIME, _runtimeProperties );

        //
        // Add System properties (command-line) as the second Properties file in list.
        //
        addProperties( PROPSOURCE_SYSTEM, System.getProperties() );

        //
        // Add standard property files next.
        //
        for ( String filename : _standardFilenames )
        {
            try
            {
                findAndLoadPropertyFile( filename );
            }
            catch ( Exception ex )
            {
                throw new PropsException( ex );
            }
        }

        //
        // Database properties are last.
        //
        addProperties( PROPSOURCE_DATABASE, getDBProperties() );
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    /**
     * Get PropsManager singleton.
     */
    public static PropsManager getInstance()
    {
        try
        {
            if ( _instance == null )
            {
                _instance = new PropsManager();
            }
            return _instance;
        }
        catch ( Exception ex )
        {
            throw new PropsException( ex );
        }
    }


    public void addProperties( String name, Properties properties )
    {
        if ( properties == null )
        {
            return;
        }

        _propertySources.put( name, properties );

        //
        // If database connection failed in the past, try again now that new properties have
        // been defined.
        //
        if ( _propertySources.get( PROPSOURCE_DATABASE ) == null )
        {
            addProperties( PROPSOURCE_DATABASE, getDBProperties() );
        }
    }

    /**
     * Allows loading of custom properties files after the default files are loaded.
     */
    public void loadPropertiesFile( String filename )
    {
        try
        {
            findAndLoadPropertyFile( filename );
        }
        catch ( Exception ex )
        {
            throw new PropsException( ex );
        }
    }

    public String getString( String key )
    {
        return getString( key, null );
    }

    public String getString( String key, String defaultValue )
    {
        String val = null;
        for ( String propName : _propertySources.keySet() )
        {
            Properties props = _propertySources.get( propName );
            val = props.getProperty( key );
            if ( val != null )
            {
                break;
            }
        }
        if ( val != null )
        {
            return val;
        }
        else
        {
            return defaultValue;
        }
    }

    public Integer getInteger( String key )
    {
        return getInteger( key, null );
    }

    public Integer getInteger( String key, Integer defaultValue )
    {
        String defaultString = null;
        String returnString = null;
        if ( defaultValue != null )
        {
            defaultString = defaultValue.toString();
        }
        returnString = getString( key, defaultString );
        if ( returnString != null )
        {
            return new Integer( returnString );
        }
        return null;
    }

    public Long getLong( String key )
    {
        return getLong( key, null );
    }

    public Long getLong( String key, Long defaultValue )
    {
        String defaultString = null;
        String returnString = null;
        if ( defaultValue != null )
        {
            defaultString = defaultValue.toString();
        }
        returnString = getString( key, defaultString );
        if ( returnString != null )
        {
            return new Long( returnString );
        }
        return null;
    }

    public Double getDouble( String key )
    {
        return getDouble( key, null );
    }

    public Double getDouble( String key, Double defaultValue )
    {
        String defaultString = null;
        String returnString = null;
        if ( defaultValue != null )
        {
            defaultString = defaultValue.toString();
        }
        returnString = getString( key, defaultString );
        if ( returnString != null )
        {
            return new Double( returnString );
        }
        return null;
    }

    public Boolean getBoolean( String key )
    {
        return getBoolean( key, null );
    }

    public Boolean getBoolean( String key, Boolean defaultValue )
    {
        String defaultString = null;
        String returnString = null;
        if ( defaultValue != null )
        {
            defaultString = defaultValue.toString();
        }
        returnString = getString( key, defaultString );
        if ( returnString != null )
        {
            return new Boolean( returnString );
        }
        return null;
    }

    public String getDecipheredString( String key )
        throws PropsException
    {
        return getDecipheredString( key, null );
    }

    public String getDecipheredString( String key, String defaultValue )
        throws PropsException
    {
        String value = getString( key, defaultValue );

        if ( value == null )
        {
            return null;
        }

        PropsCipher cipher = getCipher();
        if ( cipher == null )
        {
            return value;
        }

        return cipher.decipher( value );
    }

    public void printFileTrace()
    {
        System.out.println( "Properties Files Loaded: " );
        for ( String propertyFileName : _propertySources.keySet() )
        {
            System.out.print( "  " );
            System.out.println( ": " + propertyFileName );
        }
    }

    /**
     * Returns which property file has the given property in it.
     */
    public String getPropFile( String key )
    {
        for ( String propFileKey : _propertySources.keySet() )
        {
            Properties props = _propertySources.get( propFileKey );
            String tempVal = props.getProperty( key );
            if ( tempVal != null )
            {
                return propFileKey;
            }
        }

        return null;
    }

    /**
     * Find all property keys with the given prefix. Note that this is a case sensitive
     * comparison, but usually properties keys are all lower case.
     *
     * @param prefix The prefix to find the properties for.
     *
     * @return List of property names which match.
     */
    public List<String> findPropertyKeysByPrefix( String prefix )
    {
        List<String> propKeys = new ArrayList<String>();

        for ( Properties prop : _propertySources.values() )
        {
            for ( Object o : prop.keySet() )
            {
                try
                {
                    String propName = (String)o;
                    if ( propName.startsWith( prefix ) )
                    {
                        propKeys.add( propName );
                    }
                }
                catch ( ClassCastException e )
                {
                    // Do nothing, should never happen, but if it does, we don't care,
                    // just skip the offending key.
                }
            }
        }

        return propKeys;
    }

    public void setString( String key, String value )
    {
        _runtimeProperties.put( key, value );
    }

    public PropsCipher getCipher()
        throws PropsException
    {
        try
        {
            if ( _cipher == null )
            {
                String className = getString( Props.PROP_CIPHER );
                if ( className != null )
                {
                    _cipher = (PropsCipher)Class.forName( className ).newInstance();
                }
                else
                {
                    // Make sure to set the cipher to pass-through cipher so that it is not 
                    // attempted to be searched for over-and-over again.
                    _cipher = new PassThroughPropsCipher();
                }
            }

            return _cipher;
        }
        catch ( Exception ex )
        {
            throw new PropsException( "Unable to instantiate cipher.", ex );
        }
    }

    // ----------
    // private
    // ----------

    private void findAndLoadPropertyFile( String filename )
    {
        Properties newProp = null;
        FileInputStream inFile = null;
        InputStream inStream = null;

        //
        // Check for any files that may have been specified from the command-line via a -D.
        //
        String filenameProp = System.getProperty( filename );
        if ( filenameProp != null )
        {
            try
            {
                inFile = new FileInputStream( filenameProp );
                if ( inFile != null )
                {
                    newProp = new Properties();
                    newProp.load( inFile );
                    addProperties( "-D" + filename + "=" + filenameProp, newProp );
                }
            }
            catch ( IOException ioex )
            {
                // Ignore.
            }
        }

        //
        // Check the classpath for files.
        //
        try
        {
            inStream = getClass().getResourceAsStream( "/" + filename );
            if ( inStream != null )
            {
                newProp = new Properties();
                newProp.load( inStream );
                _propertySources.put( new String( ( getClass().getResource( "/" +
                    filename ) ).toString() ), newProp );
            }
        }
        catch ( IOException ioex )
        {
            // Ignore.
        }

        //
        // Check for file in the current directory.
        //
        try
        {
            inFile = new FileInputStream( filename );
            if ( inFile != null )
            {
                newProp = new Properties();
                newProp.load( inFile );
                _propertySources.put( new String( "." + filename ), newProp );
            }
        }
        catch ( IOException ioex )
        {
            // Ignore.
        }
    }

    private Properties getDBProperties()
    {
        try
        {
            Connection conn = createConnection();
            if ( conn == null )
            {
                return null;
            }

            Statement statement = conn.createStatement();
            Properties props = new Properties();
            ResultSet rs = statement.executeQuery( "select * from " + _dbTable + " order by name" );
            while ( rs.next() )
            {
                props.put( rs.getString( "name" ), rs.getString( "value" ) );
            }

            //
            // Cleanup (with a hammer).
            //
            rs.close();
            statement.close();
            conn.close();
            rs = null;
            statement = null;
            conn = null;

            return props;
        }
        catch ( Exception ex )
        {
            ex.printStackTrace();
            throw new PropsException( ex );
        }
    }

    private Connection createConnection()
    {
        try
        {
            // Note, these properties must exist in some properties source that comes BEFORE the 
            // database properties source of course.
            String driverClassName = getString( Props.PROP_DB_DRIVER );
            String url = getString( Props.PROP_DB_URL );
            String user = getString( Props.PROP_DB_USER );
            String password = getDecipheredString( Props.PROP_DB_PASSWORD );

            if ( driverClassName == null || driverClassName.trim().isEmpty() )
            {
                return null;
            }

            Driver driver = (Driver)Class.forName( driverClassName ).newInstance();
            Properties dbProps = new Properties();
            dbProps.setProperty( "user", user );
            dbProps.setProperty( "password", password );

            Connection connection = driver.connect( url, dbProps );
            connection.setReadOnly( true );
            connection.setAutoCommit( true );

            String _dbTable = getString( Props.PROP_DB_TABLE );
            if ( _dbTable == null || _dbTable.isEmpty() )
            {
                _dbTable = PROPVALUE_DATABASE_TABLE;
            }

            return connection;
        }
        catch ( Throwable ex )
        {
            throw new PropsException( ex );
        }
    }

    // ============================================================
    // Inner Classes
    // ============================================================

    private class PassThroughPropsCipher implements PropsCipher
    {
        public String decipher( String string )
        {
            return string;
        }
    }
}

